#!/usr/bin/env python
#Copyright (c) 2011 Isaac Dawson (WBTS Project)
#Permission is hereby granted, free of charge, to any person obtaining a copy 
#of this software and associated documentation files (the "Software"), to deal 
#in the Software without restriction, including without limitation the rights 
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
#copies of the Software, and to permit persons to whom the Software is furnished
#to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all
#copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
#THE SOFTWARE.
import os
import uuid
import cPickle

from twisted.internet import defer
from twisted.python import components, log

import storage
from wbts.storage.objectid import WBTSObjectId as ObjectId


class WBTSPydictDB(storage.WBTSStorage):
    mDbType = "pydict"
    
    def __init__(self, *connect_args):
        """
    This object is merely a container for our tables. It mimics a database.
    We return deferreds for all "user" calls.
        """
        self.mFilePath = connect_args[0]

    def connect(self):
        """
    Fakes a connection.... weee!
        """
        if (self.mFilePath == None or not isinstance(self.mFilePath, str)):
            return defer.fail(Exception("file path is not a string or empty."))
        if (os.path.exists(self.mFilePath)):
            self._load()
        return defer.succeed(0)
    
    def createTable(self, tbl_name, tbl_func=None):
        """
    Creates a new table and assigns it as an attribute. If the table already
    exists (the attribute has been set) then it returns a deferred of 1.
    @returns
    (deferred) 0 - table created.
    (deferred) 1 - already exists
        """
        
        if (tbl_name == '' or not isinstance(tbl_name, str)):
            return defer.fail(
                storage.WBTSStorageException(
                    'table name is not a string or empty.')
                )
        try:
            getattr(self, tbl_name)
            return defer.succeed(1)
        except AttributeError:
            pass

        setattr(self, tbl_name, WBTSDictTable(tbl_name))    
        return defer.succeed(0)

    def dropTable(self, tbl_name):
        """
    Gets a reference, calls drop on it, deletes the attribute, then calls
    save to save the changes to disk. Returns 0 on success, 1 on table doesn't
    exist.
        """
        ref = None
        try:
            ref = getattr(self, tbl_name)
            # call drop -> delete attribute -> save
            ref.drop()
            delattr(self, tbl_name)
            return self._save()
    
        except AttributeError:
            log.err("Unable to delete table %s as it does not exist"%tbl_name)
            pass
        return defer.succeed(1)
    
    def _save(self):
        f = None
        try:
            f = open(self.mFilePath, "wb")
        except IOError, msg:
            print "Unable to open %s for writing."%self.mFilePath
            return defer.fail(storage.WBTSStorageException(
                    'Unable to open %s for writing: %s'%(self.mFilePath,msg))
                )

        try:
            cPickle.dump(self, f)
            f.close()
        except IOError, msg:
            log.msg(msg)
            return defer.fail(1)
        return defer.succeed(0)
    
    def _load(self):
        try:
            f = open(self.mFilePath, "rb")
            old_pydict = cPickle.load(f)
            f.close()
            # take any attribute that doesn't exist in our object
            # but does exist in the unpickled one to our pydict obj.
            # Only exception is our mTableList
            tables = set(dir(old_pydict)).difference(dir(self))
            for table in tables:
                setattr(self, table, getattr(old_pydict, table))
            return True
        except IOError, msg:
            print "Unable to load file: ", self.mFilePath
            print msg
        return False        

class WBTSDictTable(storage.WBTSTableObject):
    def __init__(self, tbl_name):
        self.mTblName = tbl_name
        self.mTable = []

    def find(self, search_keys=None, skip=0, limit=-1):
        """
    Finds a splice of records (starting at skip up to skip+limit). 
    @params
    search_key - A dictionary of keys/values to be used to find records.
    skip - an integer of the # of records to skip ahead
    limit - the number of records to return, if -1 returns rest of records
            starting from skip.
    @returns
    A deferred which will return a list object of records between skip
    and skip+limit Ex: (table.records[skip:skip+limit])
        """
        if (isinstance(search_keys, dict)):
            return defer.succeed(self._find_all(search_keys))
            
        if skip > self._count() or skip < 0:
            log.msg("invalid skip value passed to find")
            return defer.succeed(None)
        
        if (limit == -1 or (skip+limit >= self._count()) ):
            return defer.succeed(self.mTable[skip:])
        return defer.succeed(self.mTable[skip:skip+limit])
    
    def find_one(self, search_keys=None):
        """
    Find a single record
    @params
    search_key - A dictionary of keys/values to be used to find a record
    @returns
    A deferred which results in a dict object of the first record found
    or an empty dict {} if nothing found.
        """

        if (search_keys == None and self._count() != 0):
            return defer.succeed(self.mTable[0])

        idx = self._find_single_search(search_keys)
        if (idx == None):
            return defer.succeed({})
        return defer.succeed(self.mTable[idx])

    def insert(self, record, safe=True):
        """
    Inserts a record (dictionary object) of data into the table.
    @params
    record - a dictionary object of our data. Keep in mind SQL versions will
    be strict about the data type, pydictdb and mongodb don't care :).
    @returns
    A deferred of 0 for success and 1 for failure.
        """
        
        if (isinstance(record, list)):
            for rec in record:
                rec["_id"] = ObjectId()
                self.mTable.append(rec)
        elif(isinstance(record, dict)):
            record["_id"] = ObjectId()
            self.mTable.append(record)
        else:
            return defer.succeed(1)
        
        return defer.succeed(0)
    
    def remove(self, search_key=None, safe=True):
        """
    Removes ANY record that maches the values from search_key.
    @params
    search_key - a dictionary of key/values to be used to find a record.
    @returns
    A deferred of 0 for success and 1 for failure or record not found.
        """
        result_idxs = self._find_multiple_search(search_key)
        # do a reverse loop so we can delete index's safely.
        try:
            for idx in reversed(result_idxs):
                del self.mTable[idx]
                
        except KeyError, msg:
            print "Error deleting tables: %s",msg
        return defer.succeed(0)
    
    def count(self):
        cnt = len(self.mTable)
        return defer.succeed(cnt)

    def drop(self):
        self.mTable = []
        return defer.succeed(0)
    
    def _find_single_search(self, search_keys):
        """
    Records are found by creating a set and seeing what intersects
    between the record and the search. An empty set basically means
    not found. Where as the search_set matching the intersection *is*
    a match and will return the first document found.
    Note: it can (and will!) match multiple records, so be careful.
    @returns
    record_idx - The index of the record or None if not found.
        """
        
        if (self._count() == 0):
            return None

        if (search_keys == None): 
            return 0 # just return first index
        
        # create a set from the items.
        search_set = set(search_keys.items())
        for idx in xrange(0, self._count()):
            record_set = set(self.mTable[idx].items())
            if (search_set.issubset(record_set)):
                return idx
        return None

    def _find_multiple_search(self, search_keys):
        if (self._count() == 0):
            return None

        # return all indexes
        if (search_keys == None):
            return [idx for idx in xrange(0, self._count())]
        

        search_set = set(search_keys.items())
        result_idx = []
        for idx in xrange(0, self._count()):
            record_set = set(self.mTable[idx].items())
            if (search_set.issubset(record_set)):
                result_idx.append(idx)

        return result_idx

    def _find_all(self, search_keys=None):
        """
    Take's a dictionary of search terms, and returns a list of records.
    
        """
        result_idxs = self._find_multiple_search(search_keys)
        results = []
        # if we get none, back just return an empty list.
        if result_idxs == None:
            return results
        
        for idx in result_idxs:
            results.append(self.mTable[idx])
        return results
    
    

    def _count(self):
        """
    For inner use only, return the real value not a deferred.
        """
        return len(self.mTable)

            
